import sys
import time
import logging
import traceback
from pprint import pformat
from datetime import datetime,timedelta
from collections import defaultdict

class AttributeEqualityMixin(object):
    def __eq__(self, other):
        if type(self) != type(other):
            return False

        attributes = [x for x in self.__dict__ if not x.startswith('__')]
        for attribute in attributes:
            if self.__dict__[attribute] != other.__dict__[attribute]:
                return False

        return True

    def __ne__(self, other):
        return not self.__eq__(other)

class ToDictionaryMixin(object):
    def to_dict(self):
        attributes = [x for x in self.__dict__ if not x.startswith('__')]

        return dict(map(lambda x: (x, self.__dict__[x]), attributes))

class BaseMessage(AttributeEqualityMixin, ToDictionaryMixin):
    pass

class AppErrorOccurredMessage(BaseMessage):
    def __init__(self, app_name, message, error, traceback, log):
        self.app_name = app_name
        self.message = message
        self.error = error
        self.traceback = traceback
        self.log = log

class HeartbeatMessage(BaseMessage):
    def __init__(self, time):
        self.time = time

class HeartbeatResponseMessage(BaseMessage):
    def __init__(self, app_name, time_responding_to, response_time):
        self.app_name = app_name
        self.time_responding_to = time_responding_to
        self.response_time = response_time

class CollectionLogHandler(logging.Handler):
    def __init__(self):
        self.log = []
        logging.Handler.__init__(self)

    def emit(self, record):
        self.log.append(record)


class ApplicationMessageBus(object):
    """Provide a message bus for a single "application".

    Handles giving the message to the correct subscribed methods and handling
    errors or results that come from those methods.
    """

    def __init__(self, app_name, subscriptions):
        """
        app_name:
            The name of the application, as a string

        subscriptions:
            The dictionary of catalyst -> application handler.
            A "catalyst" can be either a timedelta object or type of message
            object.
        """
        self.app_name = app_name

        self.message_subscriptions = defaultdict(list)
        self.idle_subscriptions = defaultdict(list)
        self.next_idle_call = {}

        self.logger = logging.getLogger(app_name)

        for catalyst, callback_list in subscriptions.items():
            if not isinstance(callback_list, list):
                callback_list = [callback_list]

            for callback in callback_list:
                if isinstance(catalyst, timedelta):
                    log_msg = "AMB Subscribing to %s on interval %s" % (callback, catalyst)
                    self.logger.debug( log_msg )

                    self.idle_subscriptions[catalyst].append(callback)
                    self.next_idle_call[catalyst] = None
                else:
                    log_msg = "AMB Subscribing to %s on message %s" % (callback, catalyst)
                    self.logger.debug( log_msg )
                    self.message_subscriptions[catalyst].append(callback)

    def _convert_to_list(self, results, non_message_handler):
        """Convert a result from a single message handler to a list.

        results:
            Returned from a handler.

        non_message_handler
            Single-argument (the value that was not a message) callable.

        While a message handler can return any of None, a single message or a
        list of messages, we want to return a single format. This will take the
        results and turn them into a list of messages, even if that list is
        empty. Will also replace non-message types returned with error messages,
        which is the result of non_message_handler.
        """
        final_results = []

        if isinstance(results, BaseMessage):
            final_results.append(results)
        elif isinstance(results, list):
            for result in results:
                if isinstance(result, BaseMessage):
                    final_results.append(result)
                else:
                    error_replacement = non_message_handler(result)
                    final_results.append(error_replacement)
        elif results != None:
            error_replacement = non_message_handler(results)
            final_results.append(error_replacement)

        return final_results
                    

    def _run_callback(self, callback, msg = None):
        """Run a single callback with a msg.

        callback:
            The callable object used as a callback.

        msg:
            The message to use as an argument, or None if there is no message
            (for a callback that's called on an interval, for example).

        Runs a callback, trapping any errors that may occur or results returned
        that aren't actually messages. Returns a list of results, or an empty
        list if no results.
        """
        log_msg = "Running callback on %s for message %s" % (callback, msg)
        self.logger.info( log_msg )

        root_logger = logging.getLogger()
        logging_handler = CollectionLogHandler()
        root_logger.addHandler(logging_handler)
        try:
            if msg:
                results = callback(msg)
            else:
                results = callback()

        except Exception as e:
            exceptionTraceback = sys.exc_info()[2]
            tb = traceback.extract_tb(exceptionTraceback)[1:]
            log = logging_handler.log

            results = AppErrorOccurredMessage(self.app_name, msg, unicode(e), tb, log)
        finally:
            def on_non_message(non_message):
                error_msg = 'Non-message %s returned' % str(non_message)
                return AppErrorOccurredMessage(self.app_name, msg, error_msg,
                        None, logging_handler.log)

            results = self._convert_to_list(results, on_non_message)
            root_logger.removeHandler(logging_handler)

        log_msg = "Results of callback %s: %s" % (callback, results)
        self.logger.info( log_msg )

        return results
    
    def _run_callbacks(self, callbacks, msg = None):
        """ Runs a collection of callbacks with a message.

        callbacks:
            A list of callable object used as a callback.

        msg:
            The message to use as an argument, or None if there is no message
            (for a callback that's called on an interval, for example).


        Runs all of the callbacks, gathering the results into a single list of
        all the messages that were results of the callback. If there were no
        resulting messages, returns an empty list.
        """
        real_results = []

        for callback in callbacks:
            results = self._run_callback(callback, msg)
            real_results.extend(results)

        return real_results

    def idle(self, datetime):
        """Checks for any handling that needs done due to time elapses.

        datetime:
            The current time.

        Should be called at semi-regular intervals in order to allow the
        ApplicationMessageBus the ability to determine when a callback whose
        catalyst is a time interval should be triggered to run. The interval at
        which this is called will determine the degree of accuracy that
        interval-based callbacks have for this ApplicationMessageBus.
        
        Returns a list of any results of interval-based callbacks.
        """
        if self.idle_subscriptions == {}:
            return []

        final_results = []

        for timedelta, subscriptions in self.idle_subscriptions.items():
            next_time = self.next_idle_call[timedelta]

            if next_time == None or next_time <= datetime:
                # Schedule next time...
                next_time = (next_time or datetime) + timedelta
                self.next_idle_call[timedelta] = next_time

                results = self._run_callbacks(subscriptions)
                final_results.extend(results)

        return final_results

    def handle(self, msg):
        """Run handlers for a given message.

        msg:
            The message to handle

        Handles a single message, returning a list of messages that are results,
        or an empty list of there are no resulting messages.
        """
        subscribers = self.message_subscriptions.get(type(msg), [])
        results =  self._run_callbacks(subscribers, msg)

        if isinstance(msg, HeartbeatMessage):
            results.append(HeartbeatResponseMessage(self.app_name, msg.time, datetime.now()))

        return results

class MainMessageBus(object):
    """
    Used as a single message bus to hold multiple ApplicationMessageBus
    objects. Can utilize a transport dependency to actually do the sending
    and recieving of messages, and call the child busses.
    """
    def __init__(self, name, transport):
        """
        name: 
            The name of the entire main application.

        transport:
            An object that implements sc_engine.messaging.transports.ITransport.
        """
        self.name = name
        self.app_busses = {}
        self.transport = transport
        self.logger = logging.getLogger(name)
        self.messages_logger = logging.getLogger('messages')

    def add_app(self, app_name, app_message_bus, message_subscriptions):
        """
        app_name:
            The name of the app being added.

        app_message_bus:
            The ApplicationMessageBus object for the app.

        message_subscriptions:
            An iterable of message types that the app bus should be subscribed to.


        Tells the transport to make ready queues for this application, and that
        the queues should subscribe to the various messages in the
        message_subscriptions list.
        """
        self.transport.add_queue(app_name)

        for message_type in message_subscriptions:
            # HACK: These shouldn't be allowed.
            if isinstance(message_type, timedelta):
                continue
            log_msg = "Subscribing to transport message %s" % message_type
            self.logger.debug( log_msg )
            self.transport.subscribe(app_name, message_type)
        self.transport.subscribe(app_name, HeartbeatMessage)

        self.app_busses[app_name] = app_message_bus

    def send(self, msg):
        """
        msg:
            The message object that should be sent.

        Sends the message over the transport.
        """
        data = (msg.__class__.__name__, pformat(msg.to_dict(), indent=5))
        log_msg = 'Sending %s\n%s' % data
        self.messages_logger.info(log_msg)

        self.transport.send(msg)

    def run(self):
        """
        Continously idles child message busses as well as checks for messages to
        retrieve from the transport to tell a child bus to handle.
        """

        handled_count = 0

        for name, app_bus in self.app_busses.items():
            outgoing_messages = app_bus.idle(datetime.now())
            map(self.send, outgoing_messages)

            with self.transport.retrieve(name) as msg:
                if not msg:
                    continue

                data = (msg.__class__.__name__, name, pformat(msg.to_dict(), indent=5))
                log_msg = 'Handling %s in %s\n%s' % data
                self.messages_logger.info(log_msg)

                results = app_bus.handle(msg)
                handled_count += 1
                outgoing_messages = results

                map(self.send, outgoing_messages)

        if handled_count == 0:
            time.sleep(3)

    def close(self):
        """
        Tells the transport to close any connections that it might have open.
        """
        self.transport.close()
        


